/**   
 * Copyright 2011 COMERE Harold
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package etrl.camera;

import com.google.gwt.dom.client.NativeEvent;

import etrl.core.TransformData;
import etrl.core.TransformManipulator;
import etrl.vecmath.Vector3f;


/**
 * \class TrackballManipulator
 * \brief Manipulates the camera to follow one entity.
 * - Left mouse button 				= Rotates around entity Y axis if ! stayOnBack
 * - Mouse wheel 					= Goes closer / farther from the entity
 * - Right mouse button 			= Rotates around entity X axis
 * - Keyboard left / right arrows 	= Rotates the manipulated transform
 * - Keyboard up / down arrows 		= Makes the manipulated transform going forward / backward
 * 
 * @author harold
 *
 */
public class TrackballManipulator extends TransformManipulator 
{
	/**
	 * left mouse button push status flag
	 */
	private boolean _leftButtonIsDown = false;
		
	/**
	 * Right mouse button push status flag
	 */
	private boolean _rightButtonIsDown = false;

	/**
	 * Left arrow key push status flag
	 */
	private boolean _leftArrowIsDown = false;
	
	/**
	 * Right arrow key push status flag
	 */
	private boolean _rightArrowIsDown = false;
	
	/**
	 * Up arrow key push status flag
	 */
	private boolean _upArrowIsDown = false;
	
	/**
	 * Down arrow key push status flag
	 */
	private boolean _downArrowIsDown = false;
	
	/**
	 * Mouse x coordinates backup
	 */
	private int _mX = 0;

	/**
	 * Mouse y coordinate backup
	 */
	private int _mY = 0;
	
	/**
	 * The followed transform.
	 */
	private TransformData _trackedTransform = null;
	
	/**
	 * Minimum distance to the entity, the camera shall not go nearest
	 */
	private float _distanceToEntityMinimum = 1.f;
	
	/**
	 * Maximum distance to the entity, the camera shall not go farest
	 */
	private float _distanceToEntityMaximum = 20.f;
	
	/**
	 * Current distance to the entity. Initialized with the average of min and max
	 */
	private float _distanceToEntity = (_distanceToEntityMinimum + _distanceToEntityMaximum) / 2.f;
	
	/**
	 * Angle done with the floor in degrees, must stay between beta_min and beta_max.
	 */
	private float _beta = 25.f;
	
	/**
	 * Minimum value allowed for the angle beta
	 */
	private float _beta_min;
	
	/**
	 * Maximum value allowed for the angle beta
	 */
	private float _beta_max;
	
	/**
	 * Angle applied around the entity Y axis
	 */
	private float _alpha = 0.f;
	
	/**
	 * Stay on back of the entity flag
	 */
	private boolean _stayOnBack;
	
	/**
	 * \brief Initializes manipulator attributes.
	 * @param[in] trackedTransform TransformData to follow.
	 * @param[in] stayOnBack If activated, the camera will stay behind and so always watch its back
	 * @param[in] betaMin Minimum allowed angle value around the X axis
	 * @param[in] betaMax Maximum allowed angle value aroud the X axis
	 */
	public TrackballManipulator(TransformData trackedTransform, boolean stayOnBack, float betaMin, float betaMax) 
	{
		super();
		
		_beta_min = betaMin;
		_beta_max = betaMax;
		
		_trackedTransform = trackedTransform;
		_stayOnBack = stayOnBack;
		
		refreshPosition();
	}

	/**
	 * {@inheritDoc}
	 */
	
	public void onMouseDown(NativeEvent event) 
	{
		switch (event.getButton())
		{
		case NativeEvent.BUTTON_LEFT:
			_leftButtonIsDown = true;
			break;

		case NativeEvent.BUTTON_RIGHT:
			_rightButtonIsDown = true;
			break;
		}

		_mX = event.getClientX();
		_mY = event.getClientY();
	}

	/**
	 * {@inheritDoc}
	 */
	
	public void onMouseUp(NativeEvent event) 
	{
		switch (event.getButton())
		{
		case NativeEvent.BUTTON_LEFT:
			_leftButtonIsDown = false;
			break;

		case NativeEvent.BUTTON_RIGHT:
			_rightButtonIsDown = false;
			break;
		}
	}

	/**
	 * {@inheritDoc}
	 */
	
	public void onMouseMove(NativeEvent event)
	{
		int mx = event.getClientX();
		int my = event.getClientY();

		int deltax = mx - _mX;
		int deltay = my - _mY;

		float sensibility = 0.1f;

		if (_leftButtonIsDown) 
		{
			boolean needRefresh = false;
			
			if(! _stayOnBack && deltax != 0) 
			{
				_alpha += (float) -deltax * sensibility;
				if(_alpha > 360.f) _alpha -= 360.f;
				if(_alpha < 0.f) _alpha += 360.f;
				
				needRefresh = true;
			}
			
			if(deltay != 0) 
			{
				_beta += deltay * sensibility;
				if(_beta > _beta_max) _beta = _beta_max;
				if(_beta < _beta_min) _beta = _beta_min;
			
				needRefresh = true;
			}
			
			if(needRefresh) refreshPosition();
		}
	
		_mX = mx;
		_mY = my;
	}

	/**
	 * {@inheritDoc}
	 */
	
	public void onMouseOut(NativeEvent event) 
	{
		// When the mouse goes out of bounds, considers that all buttons are released
		_leftButtonIsDown = false;
		_rightButtonIsDown = false;
	}
	
	/**
	 * \brief Recomputes the postion of the manipulated camera according to current params.
	 */
	private void refreshPosition()
	{
		TransformData cameraTransform;
		
		float sin_beta = (float) Math.sin(_beta * Math.PI / 180.);
		float cos_beta = (float) Math.cos(_beta * Math.PI / 180.);
		
		if(_stayOnBack)
		{
			cameraTransform = new TransformData(_trackedTransform);
			cameraTransform.setUniformScale(1.f);
			
			Vector3f entityFront = _trackedTransform.getLocalZAxis();
			entityFront.negate();
			entityFront.scale(_distanceToEntity * cos_beta);
			entityFront.y = _distanceToEntity * sin_beta;

			cameraTransform.translate(entityFront);
			cameraTransform.applyRotation(new Vector3f(0.f, 1.f, 0.f), 180.f, true);			
			cameraTransform.applyRotation(new Vector3f(1.f, 0.f, 0.f), -_beta, true);
		}
		else
		{
			cameraTransform = new TransformData();
			
			float sin_alpha = (float) Math.sin(_alpha * Math.PI / 180.);
			float cos_alpha = (float) Math.cos(_alpha * Math.PI / 180.);
			
			cameraTransform.applyRotation(new Vector3f(0.f, 1.f, 0.f), _alpha, true);
			cameraTransform.applyRotation(new Vector3f(1.f, 0.f, 0.f), -_beta, true);

			Vector3f cameraPosition = new Vector3f(sin_alpha * cos_beta * _distanceToEntity, sin_beta * _distanceToEntity, cos_alpha * cos_beta * _distanceToEntity);
			cameraPosition.add(_trackedTransform.getPosition());
			cameraTransform.translate(cameraPosition);
		}
		
		_transform.setPosition(cameraTransform.getPosition());
		_transform.setOrientation(cameraTransform.getOrientation());
		
	}

	/**
	 * {@inheritDoc}
	 */
	
	public void onMouseWheel(NativeEvent event) 
	{
		// On mouse wheel event let's bring the camera closer / farther to the entity
		// Get the wheel delta
		int delta = event.getMouseWheelVelocityY();	

		// TODO : Hardocded sensibility, Uglyyyy
		float sensibility = 0.5f;
		
		// Modify the distance to the entity according to the mouse wheel delta
		_distanceToEntity += sensibility * delta;
		
		// Let the distance in defined bounds
		if(_distanceToEntity < _distanceToEntityMinimum) _distanceToEntity = _distanceToEntityMinimum;
		if(_distanceToEntity > _distanceToEntityMaximum) _distanceToEntity = _distanceToEntityMaximum;
		
		// And then recompute the camera position
		refreshPosition();
	}

	/**
	 * {@inheritDoc}
	 */
	
	public void onKeyDown(NativeEvent event) 
	{
		switch(event.getKeyCode())
		{
		case 37 :
			_leftArrowIsDown = true;
			break;
		
		case 38 :
			_upArrowIsDown = true;
			break;
			
		case 39 :
			_rightArrowIsDown = true;
			break;
			
		case 40 :
			_downArrowIsDown = true;
			break;
		}
	}

	/**
	 * {@inheritDoc}
	 */
	
	public void onKeyUp(NativeEvent event) 
	{
		switch(event.getKeyCode())
		{
		case 37 :
			_leftArrowIsDown = false;
			break;		

		case 38 :
			_upArrowIsDown = false;
			break;
			
		case 39 :
			_rightArrowIsDown = false;
			break;
			
		case 40 :
			_downArrowIsDown = false;
			break;
		}
	}

	/**
	 * {@inheritDoc}
	 */
	
	public void onFrameEvent() 
	{
		// On new frame event, we will move the tracked entity according to user keyboard input.
		
		// If there is no keyboard input, it wont be needed to recompute the camera position
		boolean refreshNeeded = false;
		
		// If left / right keys are down, apply a rotation on the tracked entity
		if(_leftArrowIsDown && ! _rightArrowIsDown)
		{
			_trackedTransform.applyRotation(new Vector3f(0.f, 1.f, 0.f), 5.f, true);
			refreshNeeded = true;
		}
		else if(! _leftArrowIsDown && _rightArrowIsDown)
		{
			_trackedTransform.applyRotation(new Vector3f(0.f, 1.f, 0.f), -5.f, true);
			refreshNeeded = true;			
		}
		
		// If up / down keys are down, apply a translation on local Z axis to the tracked entity
		if(_upArrowIsDown && ! _downArrowIsDown)
		{
			_trackedTransform.translateOnZAxis(1.f);
			refreshNeeded = true;
		}
		else if(! _upArrowIsDown && _downArrowIsDown)
		{
			_trackedTransform.translateOnZAxis(-1.f);
			refreshNeeded = true;			
		}	
		
		// Well, finaly, refresh the camera position if the tracked entity has been modified
		if(refreshNeeded) refreshPosition();	
	}

}
